#include <stdlib.h>
#include <unistd.h>
#include <string.h> // strerror
#include <stdio.h>  // fprintf
#include <sys/types.h> // getpid thread and process id

#include "thread_pool.h"
#include "util.h"

static void *thread_do_work(void *pool);

/************************************** WRAPPER FUNCTIONS BELOW *******************************************/
// Reference: CSAPP P1000 and OS 3 Pieces
void Pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(start_rountine) (void*), void *arg) {
  int retcode;
  if((retcode = pthread_create(thread, attr, start_rountine, arg)) != 0) {
     fprintf(stderr, "pthread_craete_error: %s\n", strerror(retcode));
     exit(0);
  }
}

void Pthread_mutex_lock(pthread_mutex_t *mutex) {
   int pml;
   if((pml = pthread_mutex_lock(mutex)) != 0) {
      fprintf(stderr, "pthread_mutex_lock error: %s\n", strerror(pml));
      exit(0);
   }
}

void Pthread_mutex_unlock(pthread_mutex_t *mutex) {
   int pml;
   if((pml = pthread_mutex_unlock(mutex)) != 0) {
      fprintf(stderr, "pthread_mutex_unlock error: %s\n", strerror(pml));
      exit(0);
   }
}

void Pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t  *attr) {
   int pml;
   if((pml = pthread_mutex_init(mutex, attr)) != 0) {
      fprintf(stderr, "pthread_mutex_init error: %s\n", strerror(pml));
      exit(0);
   }
}

void Pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) {
   int pcw;
   if((pcw = pthread_cond_wait(cond, mutex)) != 0) {
      fprintf(stderr, "pthread_cond_wait error: %s\n", strerror(pcw));
      exit(0);
   }
}

void Pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t  *mutex) {
   int pci;
   if((pci = pthread_cond_init(cond, mutex)) != 0) {
      fprintf(stderr, "pthread_cond_init error: %s\n", strerror(pci));
      exit(0);
   }
}
/**************************************** WRAPPER FUNCTIONS ABOVE *******************************************/

int front = 0, back = 0;

int empty() {
    return (front == back);
}

int full(int queue_size) {
    return ((back + 1) % queue_size == front);
}

void enqueue(pool_t *pool, int connfd) { // if full should not exe it
    int newBack = (back + 1) % (pool->work_queue).length;
    ((pool->work_queue).queue)[back].connfd = connfd;
    back = newBack;

}

void dequeue(pool_t *pool, int* connfd) { // if empty should not exe it
    *connfd = ((pool->work_queue).queue)[front].connfd;
    front = (front + 1) % (pool->work_queue).length;
}

// Create a threadpool, initialize variables, etc
pool_t *pool_create(int queue_size, int num_threads) {
    pool_t* pool = (pool_t*) malloc(sizeof(pool_t));

    Pthread_mutex_init(&(pool->lock), NULL); // remember to call pthread_mutex_destroy!
    Pthread_cond_init(&(pool->get_task), NULL); // remember to call pthread_cond_destroy!
    Pthread_cond_init(&(pool->fill_task), NULL);

    pool->threads = (pthread_t*) malloc(num_threads * sizeof(pthread_t));
    (pool->work_queue).queue = (pool_task_t*) malloc(queue_size * sizeof(pool_task_t));

    (pool->work_queue).length = queue_size;
    (pool->work_queue).heap_size = 0;
    pool->thread_count = num_threads;

    int i = 0;
    for(i = 0; i < num_threads; i++)
        Pthread_create(&(pool->threads[i]), NULL, thread_do_work, (void*)pool);

    return pool;
}

// Add a task to the threadpool
int pool_add_task(pool_t *pool, int connfd) {
    int err = 0;

    Pthread_mutex_lock(&(pool->lock));

    while(full((pool->work_queue).length)) // while it is full
       Pthread_cond_wait(&(pool->fill_task), &(pool->lock));
    enqueue(pool, connfd);
    pthread_cond_signal(&(pool->get_task));

    Pthread_mutex_unlock(&(pool->lock));

    return err;
}

// Destroy the threadpool, free all memory, destroy treads, etc
int pool_destroy(pool_t *pool) {
    int err = 0;

    pthread_mutex_destroy(&(pool->lock));  // deallocate memory
    pthread_cond_destroy(&(pool->get_task)); // deinitialize a condition variable
    pthread_cond_destroy(&(pool->fill_task));

    free(pool->threads);
    free((pool->work_queue).queue);
    free(pool);

    return err;
}

// Work loop for threads. Should be passed into the pthread_create() method.
// when the thread exit, it will reap itself
// we don't need to reap it in pool_destroy
static void *thread_do_work(void *pool) {
    pthread_detach(pthread_self());

    pool_t* poolPt = (pool_t*) pool;
    int connfd = 0;

    // going into the loop means trying to pick a thread to run the task
    while(1) {
        Pthread_mutex_lock(&(poolPt->lock));

        while(empty())
            Pthread_cond_wait(&(poolPt->get_task), &(poolPt->lock));
        dequeue(poolPt, &connfd);
        pthread_cond_signal(&(poolPt->fill_task));

        Pthread_mutex_unlock(&(poolPt->lock));

        handle_connection(connfd); // calling handle_connection
    }
    // going back to next iteration means returning the thread to the pool

   return 0;
}
